﻿using System.Runtime.Versioning;
using System.Security.AccessControl;
using System.Text;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.FileProviders;

namespace FrameworkCore.Utilities;

#nullable disable

/// <summary>
/// 使用磁盘文件系统的IO函数
/// </summary>
public partial class AiBLSmartEduFileProvider : PhysicalFileProvider, IAiBLSmartEduFileProvider
{
    #region 构造函数

    /// <summary>
    /// 初始化 AiBLSmartEduFileProvider 的新实例。
    /// </summary>
    /// <param name="webHostEnvironment">托管环境</param>
    public AiBLSmartEduFileProvider(IWebHostEnvironment webHostEnvironment)
        : base(File.Exists(webHostEnvironment.ContentRootPath) ? Path.GetDirectoryName(webHostEnvironment.ContentRootPath)! : webHostEnvironment.ContentRootPath)
    {
        WebRootPath = File.Exists(webHostEnvironment.WebRootPath)
            ? Path.GetDirectoryName(webHostEnvironment.WebRootPath)
            : webHostEnvironment.WebRootPath;
    }

    #endregion

    #region 实用工具

    /// <summary>
    /// 深度优先递归删除
    /// </summary>
    /// <param name="path">路径</param>
    protected virtual void DeleteDirectoryRecursive(string path)
    {
        Directory.Delete(path, true);
        const int maxIterationToWait = 10;
        var curIteration = 0;

        // 根据文档 (https://msdn.microsoft.com/ru-ru/library/windows/desktop/aa365488.aspx)
        // System.IO.Directory.Delete 方法最终（在移除文件后）调用本地的 RemoveDirectory 函数，
        // 该函数将目录标记为“已删除”。因此，我们等待直到目录实际被删除。更多详情请参阅 https://stackoverflow.com/a/4245121
        while (Directory.Exists(path))
        {
            curIteration += 1;

            if (curIteration > maxIterationToWait)
                return;

            Thread.Sleep(100);
        }
    }

    /// <summary>
    /// 判断字符串是否是有效的服务器和共享路径的通用命名约定（UNC）。
    /// </summary>
    /// <param name="path">要测试的路径。</param>
    /// <returns><see langword="true"/> 如果路径是有效的 UNC 路径；否则为 <see langword="false"/>。</returns>
    protected static bool IsUncPath(string path)
    {
        return Uri.TryCreate(path, UriKind.Absolute, out var uri) && uri.IsUnc;
    }

    #endregion

    #region Methods

    /// <summary>
    /// 将字符串数组组合成一个路径。
    /// </summary>
    /// <param name="paths">路径的各个部分组成的数组</param>
    /// <returns>组合后的路径</returns>
    public virtual string Combine(params string[] paths)
    {
        var path = Path.Combine(paths.SelectMany(p => IsUncPath(p) ? [p] : p.Split('\\', '/')).ToArray());

        if (Environment.OSVersion.Platform == PlatformID.Unix && !IsUncPath(path))
            // 在 UNIX 系统中添加前导斜杠以正确形成路径
            path = "/" + path;

        return path;
    }

    /// <summary>
    /// 创建指定路径中的所有目录和子目录，除非它们已经存在。
    /// </summary>
    /// <param name="path">要创建的目录</param>
    public virtual void CreateDirectory(string path)
    {
        if (!DirectoryExists(path))
            Directory.CreateDirectory(path);
    }

    /// <summary>
    /// 在指定的路径中创建一个文件。
    /// </summary>
    /// <param name="path">要创建的文件的路径和名称。</param>
    public virtual void CreateFile(string path)
    {
        if (FileExists(path))
            return;

        var fileInfo = new FileInfo(path);
        CreateDirectory(fileInfo.DirectoryName);

        // 创建文件后，我们使用 'using' 语句来关闭文件。
        using (File.Create(path))
        {
        }
    }

    /// <summary>
    /// 使用深度优先递归删除目录，并处理在Windows资源管理器中打开的子目录。
    /// </summary>
    /// <param name="path">要删除的目录路径</param>
    public virtual void DeleteDirectory(string path)
    {
        ArgumentException.ThrowIfNullOrEmpty(path);

        // 可以在 https://stackoverflow.com/questions/329355/cannot-delete-directory-with-directory-deletepath-true 找到更多关于目录删除和为何使用此方法的信息

        foreach (var directory in Directory.GetDirectories(path))
        {
            DeleteDirectory(directory);
        }

        try
        {
            DeleteDirectoryRecursive(path);
        }
        catch (IOException)
        {
            DeleteDirectoryRecursive(path);
        }
        catch (UnauthorizedAccessException)
        {
            DeleteDirectoryRecursive(path);
        }
    }

    /// <summary>
    /// 删除指定的文件。
    /// </summary>
    /// <param name="filePath">要删除的文件名。不支持通配符字符。</param>
    public virtual void DeleteFile(string filePath)
    {
        if (!FileExists(filePath))
            return;

        File.Delete(filePath);
    }

    /// <summary>
    /// 确定给定路径是否指向磁盘上的现有目录
    /// </summary>
    /// <param name="path">要测试的路径</param>
    /// <returns>
    /// 如果路径指向现有目录，则为true；如果目录不存在或在尝试确定指定文件是否存在时发生错误，则为false
    /// </returns>
    public virtual bool DirectoryExists(string path)
    {
        return Directory.Exists(path);
    }

    /// <summary>
    /// 将文件或目录及其内容移动到新位置
    /// </summary>
    /// <param name="sourceDirName">要移动的文件或目录的路径</param>
    /// <param name="destDirName">
    /// sourceDirName 的新位置路径。如果 sourceDirName 是文件，则 destDirName 也必须是文件名
    /// </param>
    public virtual void DirectoryMove(string sourceDirName, string destDirName)
    {
        Directory.Move(sourceDirName, destDirName);
    }

    /// <summary>
    /// 返回在指定路径中与搜索模式匹配的文件名的可枚举集合，并可选择是否搜索子目录
    /// </summary>
    /// <param name="directoryPath">要搜索的目录路径</param>
    /// <param name="searchPattern">
    /// 用于匹配路径中文件名的搜索字符串。此参数可以包含有效的字面路径和通配符（* 和 ?）字符，但不支持正则表达式
    /// </param>
    /// <param name="topDirectoryOnly">
    /// 指定是仅搜索当前目录，还是当前目录及所有子目录
    /// </param>
    /// <returns>
    /// 包含与指定路径中指定搜索模式匹配的文件的完整名称（包括路径）的可枚举集合
    /// </returns>
    public virtual IEnumerable<string> EnumerateFiles(string directoryPath, string searchPattern,
        bool topDirectoryOnly = true)
    {
        return Directory.EnumerateFiles(directoryPath, searchPattern,
            topDirectoryOnly ? SearchOption.TopDirectoryOnly : SearchOption.AllDirectories);
    }

    /// <summary>
    /// 将现有文件复制到新文件。允许覆盖同名文件。
    /// </summary>
    /// <param name="sourceFileName">要复制的文件</param>
    /// <param name="destFileName">目标文件的名称。不能是目录</param>
    /// <param name="overwrite">如果可以覆盖目标文件，则为true；否则为false</param>
    public virtual void FileCopy(string sourceFileName, string destFileName, bool overwrite = false)
    {
        File.Copy(sourceFileName, destFileName, overwrite);
    }

    /// <summary>
    /// 确定指定的文件是否存在
    /// </summary>
    /// <param name="filePath">要检查的文件</param>
    /// <returns>
    /// 如果调用者具有所需权限并且路径包含现有文件的名称，则为true；否则为false。
    /// </returns>
    public virtual bool FileExists(string filePath)
    {
        return File.Exists(filePath);
    }

    /// <summary>
    /// 获取文件的长度（以字节为单位），如果是目录或不存在的文件则返回-1
    /// </summary>
    /// <param name="path">文件路径</param>
    /// <returns>文件的长度</returns>
    public virtual long FileLength(string path)
    {
        if (!FileExists(path))
            return -1;

        return new FileInfo(path).Length;
    }

    /// <summary>
    /// 将指定的文件移动到新位置，并提供指定新文件名的选项
    /// </summary>
    /// <param name="sourceFileName">要移动的文件的名称。可以包括相对或绝对路径</param>
    /// <param name="destFileName">文件的新路径和名称</param>
    public virtual void FileMove(string sourceFileName, string destFileName)
    {
        File.Move(sourceFileName, destFileName);
    }

    /// <summary>
    /// 返回目录的绝对路径
    /// </summary>
    /// <param name="paths">路径的各部分组成的数组</param>
    /// <returns>目录的绝对路径</returns>
    public virtual string GetAbsolutePath(params string[] paths)
    {
        var allPaths = new List<string>();

        if (paths.Any() && !paths[0].Contains(WebRootPath, StringComparison.InvariantCulture))
            allPaths.Add(WebRootPath);

        allPaths.AddRange(paths);

        return Combine(allPaths.ToArray());
    }

    /// <summary>
    /// 获取一个 System.Security.AccessControl.DirectorySecurity 对象，该对象封装了指定目录的访问控制列表 (ACL) 条目
    /// </summary>
    /// <param name="path">包含 System.Security.AccessControl.DirectorySecurity 对象的目录路径，该对象描述了文件的访问控制列表 (ACL) 信息</param>
    /// <returns>封装了文件访问控制规则的对象</returns>
    [SupportedOSPlatform("windows")]
    public virtual DirectorySecurity GetAccessControl(string path)
    {
        return new DirectoryInfo(path).GetAccessControl();
    }

    /// <summary>
    /// 返回指定文件或目录的创建日期和时间
    /// </summary>
    /// <param name="path">要获取创建日期和时间信息的文件或目录</param>
    /// <returns>
    /// 一个 System.DateTime 结构，设置为指定文件或目录的创建日期和时间。该值以本地时间表示
    /// </returns>
    public virtual DateTime GetCreationTime(string path)
    {
        return File.GetCreationTime(path);
    }

    /// <summary>
    /// 返回指定目录中与指定搜索模式匹配的子目录的名称（包括其路径）
    /// </summary>
    /// <param name="path">要搜索的目录路径</param>
    /// <param name="searchPattern">
    /// 要与路径中的子目录名称匹配的搜索字符串。此参数可以包含一组有效的文字和通配符字符，但不支持正则表达式。
    /// </param>
    /// <param name="topDirectoryOnly">
    /// 指定是仅搜索当前目录，还是当前目录和所有子目录
    /// </param>
    /// <returns>
    /// 匹配指定条件的子目录的完整名称（包括路径）的数组，如果未找到目录，则返回空数组
    /// </returns>
    public virtual string[] GetDirectories(string path, string searchPattern = "", bool topDirectoryOnly = true)
    {
        if (string.IsNullOrEmpty(searchPattern))
            searchPattern = "*";

        return Directory.GetDirectories(path, searchPattern,
            topDirectoryOnly ? SearchOption.TopDirectoryOnly : SearchOption.AllDirectories);
    }

    /// <summary>
    /// 返回指定路径字符串的目录信息。
    /// </summary>
    /// <param name="path">文件或目录的路径。</param>
    /// <returns>
    /// 指定路径的目录信息，如果路径表示根目录或为空，则返回null。如果路径不包含目录信息，则返回System.String.Empty。
    /// </returns>
    public virtual string GetDirectoryName(string path)
    {
        return Path.GetDirectoryName(path);
    }

    /// <summary>
    /// 返回指定路径字符串的仅目录名。
    /// </summary>
    /// <param name="path">目录的路径。</param>
    /// <returns>目录名。</returns>
    public virtual string GetDirectoryNameOnly(string path)
    {
        return new DirectoryInfo(path).Name;
    }

    /// <summary>
    /// 返回指定路径字符串的扩展名。
    /// </summary>
    /// <param name="filePath">要获取扩展名的路径字符串。</param>
    /// <returns>指定路径的扩展名（包括点"."）。</returns>
    public virtual string GetFileExtension(string filePath)
    {
        return Path.GetExtension(filePath);
    }

    /// <summary>
    /// 返回指定路径字符串的文件名和扩展名。
    /// </summary>
    /// <param name="path">要获取文件名和扩展名的路径字符串。</param>
    /// <returns>路径中最后一个目录字符后的字符。</returns>
    public virtual string GetFileName(string path)
    {
        return Path.GetFileName(path);
    }

    /// <summary>
    /// 返回指定路径字符串的不带扩展名的文件名。
    /// </summary>
    /// <param name="filePath">文件的路径。</param>
    /// <returns>文件名，不包括最后一个点"."及其后的所有字符。</returns>
    public virtual string GetFileNameWithoutExtension(string filePath)
    {
        return Path.GetFileNameWithoutExtension(filePath);
    }

    /// <summary>
    /// 返回指定目录中与指定搜索模式匹配的文件（包括其路径），使用一个值来确定是否搜索子目录。
    /// </summary>
    /// <param name="directoryPath">要搜索的目录路径。</param>
    /// <param name="searchPattern">
    /// 用于匹配目录中文件名的搜索字符串。此参数可以包含有效的文字路径和通配符（* 和 ?）字符的组合，但不支持正则表达式。
    /// </param>
    /// <param name="topDirectoryOnly">
    /// 指定是否搜索当前目录，还是当前目录及所有子目录。
    /// </param>
    /// <returns>
    /// 指定目录中与指定搜索模式匹配的文件的完整名称（包括路径），如果未找到文件，则返回空数组。
    /// </returns>
    public virtual string[] GetFiles(string directoryPath, string searchPattern = "", bool topDirectoryOnly = true)
    {
        if (string.IsNullOrEmpty(searchPattern))
            searchPattern = "*.*";

        return Directory.GetFileSystemEntries(directoryPath, searchPattern,
            new EnumerationOptions
            {
                IgnoreInaccessible = true,
                MatchCasing = MatchCasing.CaseInsensitive,
                RecurseSubdirectories = !topDirectoryOnly,

            });
    }

    /// <summary>
    /// 返回指定文件或目录上次访问的日期和时间
    /// </summary>
    /// <param name="path">获取访问日期和时间信息的文件或目录的路径</param>
    /// <returns>一个 System.DateTime 结构，设置为指定文件或目录上次访问的日期和时间</returns>
    public virtual DateTime GetLastAccessTime(string path)
    {
        return File.GetLastAccessTime(path);
    }

    /// <summary>
    /// 返回指定文件或目录上次写入的日期和时间
    /// </summary>
    /// <param name="path">获取写入日期和时间信息的文件或目录的路径</param>
    /// <returns>
    /// 一个 System.DateTime 结构，设置为指定文件或目录上次写入的日期和时间。
    /// 此值以本地时间表示
    /// </returns>
    public virtual DateTime GetLastWriteTime(string path)
    {
        return File.GetLastWriteTime(path);
    }

    /// <summary>
    /// 返回指定文件或目录上次写入的协调世界时 (UTC) 日期和时间
    /// </summary>
    /// <param name="path">获取写入日期和时间信息的文件或目录的路径</param>
    /// <returns>
    /// 一个 System.DateTime 结构，设置为指定文件或目录上次写入的日期和时间。
    /// 此值以 UTC 时间表示
    /// </returns>
    public virtual DateTime GetLastWriteTimeUtc(string path)
    {
        return File.GetLastWriteTimeUtc(path);
    }

    /// <summary>
    /// 在指定路径创建或打开一个文件，以进行读/写访问
    /// </summary>
    /// <param name="path">要创建的文件的路径和名称</param>
    /// <returns>一个 <see cref="FileStream"/>，提供对指定路径的文件的读/写访问</returns>
    public FileStream GetOrCreateFile(string path)
    {
        if (FileExists(path))
            return File.Open(path, FileMode.Open, FileAccess.ReadWrite);

        var fileInfo = new FileInfo(path);
        CreateDirectory(fileInfo.DirectoryName);

        return File.Create(path);
    }

    /// <summary>
    /// 检索指定路径的父目录
    /// </summary>
    /// <param name="directoryPath">要检索其父目录的路径</param>
    /// <returns>父目录的路径，如果路径是根目录（包括 UNC 服务器或共享名称的根），则返回 null</returns>
    public virtual string GetParentDirectory(string directoryPath)
    {
        return Directory.GetParent(directoryPath).FullName;
    }

    /// <summary>
    /// 从物理磁盘路径获取虚拟路径
    /// </summary>
    /// <param name="path">物理磁盘路径</param>
    /// <returns>虚拟路径，例如 "~/bin"</returns>
    public virtual string GetVirtualPath(string path)
    {
        if (string.IsNullOrEmpty(path))
            return path;

        if (!IsDirectory(path) && FileExists(path))
            path = new FileInfo(path).DirectoryName;

        path = path?.Replace(WebRootPath, string.Empty).Replace('\\', '/').Trim('/').TrimStart('~', '/');

        return $"~/{path ?? string.Empty}";
    }

    /// <summary>
    /// 检查路径是否为目录
    /// </summary>
    /// <param name="path">要检查的路径</param>
    /// <returns>如果路径是目录，则为 true；否则为 false</returns>
    public virtual bool IsDirectory(string path)
    {
        return DirectoryExists(path);
    }

    /// <summary>
    /// 将虚拟路径映射到物理磁盘路径
    /// </summary>
    /// <param name="path">要映射的路径，例如 "~/bin"</param>
    /// <returns>物理路径，例如 "c:\inetpub\wwwroot\bin"</returns>
    public virtual string MapPath(string path)
    {
        path = path.Replace("~/", string.Empty).TrimStart('/');

        // 如果虚拟路径以斜杠结尾，在转换为物理路径后也应该有斜杠结尾
        var pathEnd = path.EndsWith('/') ? Path.DirectorySeparatorChar.ToString() : string.Empty;

        return Combine(Root ?? string.Empty, path) + pathEnd;
    }
    /// <summary>
    /// 将文件的内容读取到字节数组中
    /// </summary>
    /// <param name="filePath">要读取的文件路径</param>
    /// <returns>
    /// 表示异步操作的任务
    /// 任务结果包含包含文件内容的字节数组
    /// </returns>
    public virtual async Task<byte[]> ReadAllBytesAsync(string filePath)
    {
        return File.Exists(filePath) ? await File.ReadAllBytesAsync(filePath) : Array.Empty<byte>();
    }

    /// <summary>
    /// 打开文件，使用指定编码读取文件的所有行，然后关闭文件。
    /// </summary>
    /// <param name="path">要打开以进行读取的文件</param>
    /// <param name="encoding">应用于文件内容的编码</param>
    /// <returns>
    /// 表示异步操作的任务
    /// 任务结果包含包含文件所有行的字符串
    /// </returns>
    public virtual async Task<string> ReadAllTextAsync(string path, Encoding encoding)
    {
        await using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
        using var streamReader = new StreamReader(fileStream, encoding);

        return await streamReader.ReadToEndAsync();
    }

    /// <summary>
    /// 打开文件，使用指定编码读取文件的所有行，然后关闭文件。
    /// </summary>
    /// <param name="path">要打开以进行读取的文件</param>
    /// <param name="encoding">应用于文件内容的编码</param>
    /// <returns>包含文件所有行的字符串</returns>
    public virtual string ReadAllText(string path, Encoding encoding)
    {
        using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
        using var streamReader = new StreamReader(fileStream, encoding);

        return streamReader.ReadToEnd();
    }

    /// <summary>
    /// 将指定的字节数组写入文件
    /// </summary>
    /// <param name="filePath">要写入的文件路径</param>
    /// <param name="bytes">要写入文件的字节数组</param>
    /// <returns>表示异步操作的任务</returns>
    public virtual async Task WriteAllBytesAsync(string filePath, byte[] bytes)
    {
        await File.WriteAllBytesAsync(filePath, bytes);
    }

    /// <summary>
    /// 创建新文件，使用指定编码将指定字符串写入文件，然后关闭文件。如果目标文件已存在，则将其覆盖。
    /// </summary>
    /// <param name="path">要写入的文件路径</param>
    /// <param name="contents">要写入文件的字符串</param>
    /// <param name="encoding">应用于字符串的编码</param>
    /// <returns>表示异步操作的任务</returns>
    public virtual async Task WriteAllTextAsync(string path, string contents, Encoding encoding)
    {
        await File.WriteAllTextAsync(path, contents, encoding);
    }

    /// <summary>
    /// 创建新文件，使用指定编码将指定字符串写入文件，然后关闭文件。如果目标文件已存在，则将其覆盖。
    /// </summary>
    /// <param name="path">要写入的文件路径</param>
    /// <param name="contents">要写入文件的字符串</param>
    /// <param name="encoding">应用于字符串的编码</param>
    public virtual void WriteAllText(string path, string contents, Encoding encoding)
    {
        File.WriteAllText(path, contents, encoding);
    }

    /// <summary>定位给定路径的文件。</summary>
    /// <param name="subpath">标识文件的相对路径。</param>
    /// <returns>文件信息。调用方必须检查 Exists 属性。</returns>
    public new virtual IFileInfo GetFileInfo(string subpath)
    {
        subpath = subpath.Replace(Root, string.Empty);

        return base.GetFileInfo(subpath);
    }

    #endregion

    #region 属性

    /// <summary>
    /// 获取或设置包含可用于网络服务的应用程序内容文件的目录的绝对路径。
    /// </summary>
    public string WebRootPath { get; }

    #endregion
}